package se.webbzon.boltzmann.game;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;

import se.webbzon.boltzmann.login.AccessException;
import se.webbzon.boltzmann.login.HTTPGetRequest;
import se.webbzon.boltzmann.login.HTTPPostRequest;
import se.webbzon.boltzmann.login.HTTPRequest;
import se.webbzon.boltzmann.login.HTTPResponse;
import se.webbzon.boltzmann.login.LoginAccess;
import se.webbzon.oschi01.aes.HexInputStream;
import se.webbzon.oschi01.aes.HexOutputStream;
import se.webbzon.oschi01.aes.RijndaelBuilder;
import se.webbzon.oschi01.aes.RijndaelCipher;
import se.webbzon.oschi01.aes.RijndaelMatrix;
import se.webbzon.oschi01.aes.RijndaelParser;

public class OnlineGameManager implements GameManager {
	
	// A UTF-8 character set used for encoding/decoding strings
	Charset utf8 = Charset.forName("UTF-8");
	
	// The default HTTP timeout
	private static final int defaultTimeout = 10000;
	
	// The login access for this online game manager
	private final LoginAccess access;
		
	// A list of all the available licenses and games
	private final Map<Integer,OnlineLicense> licenses;
	private final List<OnlineGame> games;
	
	// The maximal number of games that may be saved
	int maxGames;
	
	// The HTTP timeout for this online game manager
	private int timeout;
		
	/*============================================================
	Constructors
	============================================================*/
		
	public OnlineGameManager(LoginAccess access) {
		this.access = access;
		licenses = new HashMap<Integer,OnlineLicense>();
		games = new ArrayList<OnlineGame>();
		maxGames = 0;
		timeout = defaultTimeout;
	}
		
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Sets the HTTP read timeout for this OnlineGameManager. The provided time is 
	 * must be given in milliseconds. **/
	public void setReadTimeout(int timeout) {
		this.timeout = timeout;
	}
	
	/** Returns the HTTP read timeout for this OnlineGameManager. The returned time is
	 * given in milliseconds. **/
	public int getReaderTimeout() {
		return timeout;
	}
	
	@Override public void refresh() throws AccessException, IOException {
		URL url = new URL("http://" + access.getDomain() + "/boltzmann/listgames.php");
		RijndaelCipher cipher = new RijndaelCipher();
		cipher.setKey(RijndaelMatrix.createKey(access.getHashword(),0));
		attemptRefresh(cipher,url,true);
	}

	@Override public int maxGames() {
		return maxGames;
	}

	@Override public int games() {
		return games.size();
	}

	@Override public Game getGame(int n) {
		return games.get(n);
	}

	@Override public Game createGame(String name) throws AccessException, IOException, MaxGamesReachedException {
		URL url = new URL("http://" + access.getDomain() + "/boltzmann/savegame.php");
		RijndaelCipher cipher = new RijndaelCipher();
		cipher.setKey(RijndaelMatrix.createKey(access.getHashword(),0));
		
		// Create a stack of all the licenses which can have new games
		Stack<OnlineLicense> licenses = new Stack<OnlineLicense>();
		for (Entry<Integer,OnlineLicense> entry : this.licenses.entrySet()) {
			OnlineLicense license = entry.getValue();
			if (license.getCurrentGames() < license.getMaxGames() &&
				(license.isActive() || license.getFreeExpire().before(new Date())))
				licenses.push(license);
		}
		
		if (licenses.isEmpty())
			throw new MaxGamesReachedException("No license is available!");
		
		OnlineGame game = attemptCreateGame(name,cipher,url,licenses);
		if (game != null) {
			games.add(game);
			OnlineLicense license = this.licenses.get(game.getId());
			if (license != null)
				license.setCurrentGames(license.getCurrentGames() + 1);
		}
			
		return game;
	}

	@Override public boolean removeGame(int n) throws AccessException, IOException {
		OnlineGame game = games.get(n);
		if (game != null) {
			URL url = new URL("http://" + access.getDomain() + "/boltzmann/savegame.php");
			String query = "game_method=delete&game_id=" + game.getId();
			RijndaelCipher cipher = new RijndaelCipher();
			cipher.setKey(RijndaelMatrix.createKey(access.getHashword(),0));
			RijndaelBuilder builder = new RijndaelBuilder();
			builder.write(query.getBytes(utf8));
			byte[] decBytes = new byte[builder.size()];
			builder.writeTo(decBytes, 0);
			RijndaelMatrix[] matrices = RijndaelMatrix.fromBytes(decBytes, 0, decBytes.length);
			cipher.encrypt(matrices);
			String enc = HexOutputStream.toHex(decBytes);
			if (attemptDelete(enc, url, true)) {
				games.remove(n);
				return true;
			} else
				return false;
		} else
			return false;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Attempts to refresh the game list by using the given RijndaelCipher. 
	 * The game list will be retrieved from the given url. If refreshOnFail is set
	 * then the login access will be refreshed once if the access is denied by the server. **/
	private final void attemptRefresh(RijndaelCipher cipher, URL url, boolean refreshOnFail) throws AccessException, IOException {
		String response = null, message = null;
		
		// Retrieve list from server
		try (HTTPRequest req = new HTTPGetRequest(url,access); HTTPResponse res = req.send()) {
			response = res.readLine();
			message = response != null ? res.readLine() : null;
		} catch (IOException e) {
			throw e;
		}
			
		// Check the response
		if (response == null)
			throw new IOException("Exception was thrown due to invalid server response!");
		else if (response.equals("NO")) {
			// Attempt to refresh the login access on fail
			if (refreshOnFail) {
				access.refresh();
				attemptRefresh(cipher,url,false);
			} else
				throw new AccessException("Access was denied by server!");
		} else if (response.equals("OK")) {
			// Decrypt and parse the game list
			if (message.length() % 32 == 0) {
				byte[] enc = HexInputStream.fromHex(message);
				RijndaelMatrix[] matrices = RijndaelMatrix.fromBytes(enc,0,enc.length);
				cipher.decrypt(matrices);
				RijndaelParser parser = new RijndaelParser(enc,0,enc.length);
				BufferedReader gameListReader = new BufferedReader(new InputStreamReader(parser.getInputStream()));
				parseList(gameListReader);
			} else
				throw new IOException("Exception was thrown due to invalid server response!");
		} else
			throw new IOException("Exception was thrown due to invalid server response!");
	}
	
	/** Attempts to create a new game by using the given RijndaelCipher on the server
	 * with the given url. **/
	private final OnlineGame attemptCreateGame(String name, RijndaelCipher cipher, URL url, Stack<OnlineLicense> licenses) throws AccessException, IOException, MaxGamesReachedException {
		if (licenses.size() <= 0)
			throw new MaxGamesReachedException("No license is available!");
		else {
			OnlineLicense license = licenses.pop();
			String response = null, message = null;
			
			RijndaelBuilder builder = new RijndaelBuilder();
			String dec = "game_method=create&license_id=" + license.getId() + "&game_name=" + URLEncoder.encode(name, "UTF-8");
			builder.write(dec.getBytes(utf8));
			byte[] decBytes = new byte[builder.size()];
			builder.writeTo(decBytes, 0);
			RijndaelMatrix[] matrices = RijndaelMatrix.fromBytes(decBytes, 0, decBytes.length);
			cipher.encrypt(matrices);
			String enc = HexOutputStream.toHex(decBytes);
			
			// Retrieve list from server
			try (HTTPPostRequest req = new HTTPPostRequest(url,access)) {
				req.addField("submitter_submit", enc);
				try (HTTPResponse res = req.send()) {
					response = res.readLine();
					message = response != null ? res.readLine() : null;
				}
			} catch (IOException e) {
				throw e;
			}
			
			// Check the response
			if (response == null)
				throw new IOException("Exception was thrown due to invalid server response!");
			else if (response.equals("NO")) {
				int code = getErrorCode(message);
				if (code == 2) { 
					access.refresh();
					licenses.push(license);
					return attemptCreateGame(name,cipher,url,licenses);
				} else if (code == 5) {
					return attemptCreateGame(name,cipher,url,licenses);
				} else
					throw new IOException("Exception was thrown due to invalid server response!");
			} else if (response.equals("OK")) {
				return new OnlineGame(access,Integer.parseInt(message),license.getId(),0,name);
			} else
				throw new IOException("Exception was thrown due to invalid server response!");
		}
	}
	
	/** Attempts to delete the game with the given id. **/
	private final boolean attemptDelete(String query, URL url, boolean refreshOnFail) throws AccessException, IOException {
		String response = null, message = null;
		
		// Retrieve list from server
		try (HTTPPostRequest req = new HTTPPostRequest(url,access)) {
			req.addField("submitter_submit",query);
			try (HTTPResponse res = req.send()) {
				response = res.readLine();
				message = response != null ? res.readLine() : null;
			}	
		} catch (IOException e) {
			throw e;
		}
		
		// Check the response
		if (response == null)
			throw new IOException("Exception was thrown due to invalid server response!");
		else if (response.equals("NO") && getErrorCode(message) == 2) {
			// Attempt to refresh the login access on fail
			access.refresh();
			return attemptDelete(query,url,false);
		} else if (response.equals("OK")) {
			return true;
		} else
			throw new IOException("Exception was thrown due to invalid server response!");
	}
	
	/** Parses the game list by using the given buffered reader. **/
	private final void parseList(BufferedReader gameListReader) throws IOException {
		licenses.clear();
		games.clear();
		maxGames = 0;
		final Date now = new Date();
		// Parse the licenses
		final int nrOflicenses = Integer.parseInt(gameListReader.readLine());
		for (int i = 0; i < nrOflicenses; i++) {
			String[] license = gameListReader.readLine().split(" ");
			int id = Integer.parseInt(license[0]);
			boolean active = license[1].equals("1");
			Date freeExpire = new Date(Long.parseLong(license[2]));
			int maxGames = Integer.parseInt(license[3]);
			licenses.put(id,new OnlineLicense(id,active,freeExpire,0,maxGames));
			
			if (now.before(freeExpire) || active)
				maxGames += maxGames;
		}
		// Parse the games
		final int nrOfGames = Integer.parseInt(gameListReader.readLine());
		for (int i = 0; i < nrOfGames; i++) {
			String game = gameListReader.readLine();
			int firstSpace = game.indexOf(' ',0);
			int secondSpace = game.indexOf(' ',firstSpace+1);
			int thirdSpace = game.indexOf(' ',secondSpace+1);
			int id = Integer.parseInt(game.substring(0, firstSpace));
			int licenseId = Integer.parseInt(game.substring(firstSpace+1, secondSpace));
			int revision = Integer.parseInt(game.substring(secondSpace+1, thirdSpace));
			String name = game.substring(thirdSpace);
			games.add(new OnlineGame(access,id,licenseId,revision,name));
			
			OnlineLicense license = licenses.get(id);
			if (license != null)
				license.setCurrentGames(license.getCurrentGames() + 1);
		}
	}
	
	/** Returns the error code of an error message. **/
	private static final int getErrorCode(String message) {
		if (message == null)
			return 0;
		int fromIndex = message.indexOf('(') + 1;
		int endIndex = message.indexOf(')');
		return Integer.parseInt(message.substring(message.indexOf(' ', fromIndex) + 1,endIndex));
	}

}
